Skip to content

Vue ref、reactive、nextTick

作者:青见春山
发表于:2026-07-29
字数统计:5500 字
预计阅读19分钟

涵盖 Vue 中 ref 的两个含义、ref vs reactive 选型、nextTick 原理与微任务的关系。原文件"Vue ref"和"Vue nextTick"两节合并到本文。

一、Vue ref 的两层含义

Vue 中的 ref 有两个主要用途:一是用于获取 DOM 元素或组件实例的引用;二是作为响应式数据的 API(ref 函数),用来创建响应式变量。两者是不同层面的概念,但名字容易混淆。

模板引用(ref 属性)

在模板中给元素或组件加 ref="xxx",在组件实例中通过 this.$refs.xxx(选项式)或获取同名 ref(组合式)来访问 DOM 元素或子组件实例。常用于:

  • 手动操作 DOM(比如聚焦输入框、获取元素宽高)
  • 调用子组件方法(比如表单组件暴露验证方法)

响应式 API(ref 函数)

在 Vue 3 组合式 API 中,ref() 用来创建一个响应式变量,包裹基本类型或对象,通过 .value 访问和修改。Vue 2 中也有 ref,但主要用于选项式 API 的模板引用。

两者关系

在 Vue 3 中,模板中的 ref 属性和响应式 ref 函数是独立的概念,但底层都依赖响应式系统。模板引用实际上也是一个 ref 对象,当组件挂载后,该 ref 的值会被赋值为 DOM 元素或组件实例。

使用上要注意:模板引用是挂载后才能访问,要在 mounted 生命周期或 onMounted 中获取;Vue 3 中用 useTemplateRef(3.5+)或直接定义同名 ref 变量接收;组件上的 ref 默认获取的是组件实例,但使用了 <script setup> 的组件默认是私有的,需要用 defineExpose 暴露方法或属性才能被父组件访问。

二、20 秒极简版

Vue 中 ref 有两层含义:一是模板引用,通过 ref 属性获取 DOM 或组件实例,常用于手动操作 DOM 或调用子组件方法;二是响应式 API,ref() 创建响应式变量。Vue 3 中两者都是响应式的,但使用场景不同。

三、ref 和 reactive 的区别?怎么选?

核心区别在于数据类型、访问方式、解构响应性

ref

  • 可以包裹任何类型(基本类型或对象)
  • 通过 .value 访问
  • 在模板中自动解包,不需要写 .value
  • 适合用于基本类型,或者需要重新赋值的场景(比如 user.value = newUser

reactive

  • 只能包裹对象类型(对象、数组、Map 等)
  • 直接访问属性,不需要 .value
  • 不支持直接重新赋值整个对象,否则会丢失响应性

选型原则

  • 基本类型 → 必须用 ref
  • 对象类型,且不需要整体替换 → 可以用 reactive,写起来更简洁
  • 对象类型,需要整体替换 → 必须用 ref(或 reactive + Object.assign 但麻烦)
  • 需要解构或传给函数 → ref 更安全,reactive 解构后需要 toRefs

个人习惯:统一用 ref,这样心智负担小,不需要在两个 API 之间反复切换,团队协作也更一致。

四、v-for 中怎么获取多个 DOM 元素?

Vue 2 中

v-for 中使用 ref 会自动把多个元素收集成一个数组,通过 this.$refs.xxx 获取。

Vue 3 中

需要手动处理。有两种方式:

方式一:用函数形式的 ref(推荐)

Plain
<div v-for="item in list" :ref="(el) => setItemRef(el, item.id)"></div>

setItemRef 中把元素存到响应式对象或 Map 里。

方式二:用 ref 变量 + 数组 push(Vue 3.5 之前)

JavaScript
const itemRefs = ref([])
const setRef = (el) => { if (el) itemRefs.value.push(el) }

注意需要在数据变化时重置数组,否则会有重复元素。

Vue 3.5+ 的简化:新版本中 useTemplateRef 配合 v-for 体验有所改善,但核心还是需要手动收集。

五、<script setup> 的组件 ref 为什么拿不到内部方法?

原因是为了封装性和安全性。在 <script setup> 语法下,组件默认是"关闭"的,不会把内部的方法、数据暴露给父组件,避免父组件直接操作子组件内部状态导致耦合。

解决方案:使用 defineExpose 显式暴露

JavaScript
// 子组件
const validate = () => { /* 验证逻辑 */ }
const reset = () => { /* 重置逻辑 */ }
defineExpose({ validate, reset })

父组件拿到 ref 后,就可以调用 childRef.value.validate() 了。

这个设计其实很好,强制组件之间的交互显式化,避免隐式依赖,代码更可维护。如果父组件需要频繁调用子组件方法,说明设计可能需要重新审视——也许应该通过 props 和 events 驱动,而不是 ref 调用。

六、$nextTick 原理

标准面试回答(1 分钟)

$nextTick 是 Vue 提供的异步更新后的回调机制,用于在 DOM 更新完成后执行操作。核心价值是解决"数据变化后立即操作 DOM 拿不到最新结果"的问题。

原理上,Vue 的响应式数据更新是异步批量处理的。当你修改数据时,Vue 不会立即更新 DOM,而是将更新任务推入异步队列,在同一个事件循环中合并所有数据变更,然后一次性更新 DOM。这样做是为了避免频繁的 DOM 操作,提升性能。

$nextTick 的作用就是在这次 DOM 更新完成后执行回调,确保你获取到的是最新的 DOM 状态。

使用场景

  1. 数据变化后需要操作 DOM:比如获取更新后的元素宽高、滚动位置、手动聚焦
  2. 组件挂载后立即操作 DOMmounted 钩子中不能保证子组件渲染完成,有时需要 $nextTick 包裹
  3. 在 Watcher 回调中拿到最终 DOM 状态

Vue 2 和 Vue 3 都支持 $nextTick,Vue 3 中还提供了独立的 nextTick 函数,支持 Promise 写法,可以用 await nextTick() 更优雅地等待 DOM 更新。

20 秒极简版

$nextTick 是 Vue 的 DOM 更新后回调机制。因为 Vue 的 DOM 更新是异步批量的,数据变化后 DOM 不会立即更新,用 $nextTick 可以等 DOM 渲染完再执行操作。常用于获取更新后的元素尺寸、滚动位置,或确保子组件挂载完成。

七、异步更新队列怎么实现的?为什么是异步?

Vue 的异步更新机制核心是将数据变更触发的 DOM 更新任务推入队列,批量执行

具体流程:每次数据变化时,对应的 Watcher 会被推入一个队列,如果在同一个 tick 中有多次数据变化,同一个 Watcher 只会被推入一次(去重)。然后 Vue 会通过 nextTick 在微任务(或降级方案)中统一执行队列里的所有更新任务。

为什么是异步? 主要是性能考虑。比如在同一个函数里连续修改数据的多个属性,如果每次修改都同步更新 DOM,会造成大量无意义的渲染。异步批量更新可以把这些变更合并成一次渲染,大幅提升性能。同时也避免了开发者手动管理渲染时机,让数据驱动视图的体验更流畅。

八、$nextTick 和 setTimeout 的区别?

核心区别是执行时机不同$nextTick 默认使用微任务(Promise 或 MutationObserver),setTimeout 是宏任务。微任务在宏任务之前执行,所以 $nextTick 的回调会先于 setTimeout 执行。

在 Vue 的更新流程中,数据变化后,Vue 会把 DOM 更新任务也放到 nextTick 队列里。所以当你调用 this.$nextTick(callback) 时,这个 callback 会被放到同一个微任务队列中,排在 DOM 更新任务之后,确保 DOM 更新完成后才执行。

如果用一个比喻:Vue 的 DOM 更新是"一号微任务",你传入的 $nextTick 回调是"二号微任务",而 setTimeout 是"下一轮宏任务"。所以执行顺序:同步代码 → DOM 更新 → $nextTick 回调 → setTimeout 回调。

Vue 3 中 nextTick 也是基于 Promise 的微任务实现,只有在不支持 Promise 的旧环境才会降级到 setTimeout

九、mounted 里直接操作 DOM 行不行?

mounted 钩子执行时,组件自身的 DOM 已经挂载完成,可以直接操作。但有一个坑:子组件可能还没渲染完成

具体分情况:

  • 如果操作的是当前组件模板中的 DOM 元素mounted 里直接操作没问题,因为当前组件的根 DOM 已经渲染好了。
  • 如果操作的是子组件内部的 DOM 元素,或者通过 ref 获取子组件实例后调用子组件方法,且子组件内部依赖数据异步渲染,那么直接操作可能会失败,需要 $nextTick 确保子组件也更新完毕。

典型场景:mounted 里想获取子组件的元素高度,如果子组件的高度由 props 数据决定,而这个 props 可能在父组件 mounted 时还没传递给子组件(虽然看起来传了,但子组件的渲染是异步的),就需要 await nextTick()this.$nextTick 等待子组件渲染完成。

经验:只要操作的是 DOM,且对时机有严格要求,加个 $nextTick 会更稳妥,不会有什么性能损失,但能避免一些边界情况的 bug。

十、Vue nextTick 与微任务的反直觉表现

完整版本见 异步与事件循环,本节为 Vue 视角的概要。

假设同步代码依次触发:

JavaScript
this.msg = 'hello'                            // 1. 触发 DOM 更新
Promise.resolve().then(() => console.log('普通微任务'))  // 2. 注册普通微任务
this.$nextTick(() => console.log('nextTick'))  // 3. 注册 nextTick

Vue 2 中的表现

结果:DOM 更新 → nextTick → 普通微任务

Vue 2 第一次调用底层 Promise.resolve().then(flushCallbacks) 把清空数组的操作送入了宿主环境的微任务队列。后续 nextTick 只把回调推入内部 callbacks 数组,不会再去生成新的底层 Promise 微任务。运行环境开始执行微任务检查点时,先拿出第一个微任务(Vue 安排的),依次执行 DOM 更新 和 nextTick,然后拿出第二个微任务,执行 普通微任务

Vue 3 中的表现(反直觉,面试绝杀点)

结果:DOM 更新 → 普通微任务 → nextTick

Vue 3 把 DOM 更新推入内部任务队列,并调用 Promise.resolve().then(flushJobs) 将 DOM 更新送到浏览器的微任务队列中。同时,Vue 3 在内部保存了这个 Promise 实例(currentFlushPromise)。

nextTick 源码实现就是通过 .then 挂接在 currentFlushPromise 后面的。浏览器执行第一个微任务 flushJobs 完成 DOM 更新后,currentFlushPromise resolve 了,导致 nextTick 的回调作为新的微任务推入队列尾部。此时微任务队列里排队的是刚刚的"普通微任务",于是执行普通微任务,最后执行 nextTick。

满分面试回答

Vue 中的 DOM 更新、普通微任务、nextTick 谁先执行,不能一概而论,因为它们都依赖 JavaScript 的微任务队列,并且在 Vue2 和 Vue3 的底层调度逻辑有着根本的差异。

共同点:在本例“先触发组件更新、再注册 nextTick”的前提下,待处理 DOM 更新会先于相应的 nextTick 回调完成;该结论依赖调用时机,不应脱离上下文写成绝对顺序。

Vue2:DOM 更新 → nextTick → 普通微任务。因为 Vue2 用一个内部 callbacks 数组收集当前刻发生的所有更新和 nextTick,然后用唯一一个底层 Promise 微任务去清空它。

Vue3:DOM 更新 → 普通微任务 → nextTick。因为 Vue3 的 nextTick 是通过 Promise 链式调用挂接到 flushJobs 之后,nextTick 回调会在 DOM 更新(微任务 1)完成之后,才被重新推入底层微任务队列。

结论:并没有所谓的 Vue 专有高优先级队列,本质全都是 Event Loop 对于 .then 注册顺序和链式调用的时序比拼。

十一、易错点

ref 章节

  • 混淆"模板引用"和"响应式 ref":面试官问"ref 是什么"如果只答模板引用,说明对 Vue 3 响应式 API 不熟
  • 忘记生命周期要求:说"用 ref 获取 DOM 元素"但不说"要在 mounted 之后"
  • Vue 3 中忘了 .value
  • <script setup> 组件不暴露的问题:答不上 defineExpose
  • v-for 多 ref 的处理方式过时:不知道 Vue 3 需要手动处理
  • ref 和 reactive 选型说不出理由:只说"都用 ref"没有给出清晰的选择逻辑

nextTick 章节

  • 误以为 $nextTick 是"延迟执行":等同于 setTimeout(fn, 0)
  • 混淆"数据更新"和"DOM 更新":响应式数据赋值通常同步生效,而对应 DOM 更新会被批量调度到后续刷新时机
  • 忘记 Vue 3 的 nextTick 支持 Promise
  • 不知道 Vue 的更新队列是微任务
  • 忽略 $nextTick 在测试中的使用
  • 场景举例太简单:能结合具体项目经验更真实

十二、关联文档